﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Diagnostics;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.StorageClient;
using DDFileSyncPlugin;
using System.ServiceModel;
using System.Threading.Tasks;
using System.IO;
using System.Web;
using smarx.WazStorageExtensions;
namespace DDFileSyncPlugin
{
    public enum SyncType : short
    {
        /// <summary>
        /// No sync of any type
        /// </summary>
        SyncOff = 0,

        /// <summary>
        /// Only synchronize to storage periodically. Ignore any file events
        /// </summary>
        PeriodicSyncToStorageOnly = 1,
      
        /// <summary>
        /// Sync the folder only if a remote file change event is received. This does not use periodic sync at all. Does not replicate files. Relies on storage sync to provide the latest update.
        /// </summary>
        SyncToStorageOnlyOnFileEvent = 2,
       
        /// <summary>
        /// Sync to storage periodically (long period recommended) and also replicate files to other instances on changes (most aggressive as it uses both)
        /// </summary>
        PeriodicSyncToStorageAndReplication = 3,

        /// <summary>
        /// The system only replicates the files on nodes. Does not synchronize with storage account. May be useful if using cloud drives.
        /// Does not sync with storage account. Good for replicated transient data or cloud drives.
        /// </summary>
        ReplicationOnly = 4

    

    }
   public class FileSyncServiceHost
    {
       private const string BACKUP_CONTAINER_NAME_TEMPLATE = "ddbackupfiles";
        /// <summary>Channel factory for inter-role notifications.</summary>
        private ChannelFactory<IFileSync> factory;

        /// <summary>ServiceHost object for internal endpoints.</summary>
        private ServiceHost serviceHost;

        private FileChangeReceivingService fileChangeSvc;

        private Dictionary<string, FileSystemWatcher> FolderWatchers;  

     

        private CloudStorageAccount _fileStorageAccount;

        public CloudStorageAccount FileStorageAccount
        {
            get { return _fileStorageAccount; }
            set { _fileStorageAccount = value; }
        }

        private SyncType _ddSyncType = SyncType.SyncOff;

        public SyncType DdSyncType
        {
            get { return _ddSyncType; }
            set { _ddSyncType = value; }
        }

        private bool localTesting = false;
        private bool backupFileOnUpdateDelete = true;

        #region Initialize
        private void InitFileSystemWatchers()
        {
            FolderWatchers = new Dictionary<string, FileSystemWatcher>();
            foreach (string folder in SyncService.Instance.GlobalAbsolutePathOfFoldersToInclude)
            {
                FileSystemWatcher fw = EnableFileSystemWatcher(new FileSystemWatcher(), folder);

                FolderWatchers.Add(folder, fw);

            }

        }
        public void StartService(int retries)
       {
           try
           {
            //initialize file watchers
           Trace.TraceInformation("Initializing File Watchers.");
           InitFileSystemWatchers();
           Trace.TraceInformation("Initialized File Watchers.");
           // recycle the role if host cannot be started 
           // after the specified number of retries
           if (retries == 0)
           {
             //  RoleEnvironment.RequestRecycle();
               Trace.TraceError("Retries exceeded with fatal errors. Retry count: {0}", retries);
               return;
           }

           Trace.TraceInformation("Starting file sync service host...");
           fileChangeSvc = new FileChangeReceivingService();

           Trace.TraceInformation("Initialized FileChangeReceivingService");
           fileChangeSvc.FileChanged += new FileChangeReceivingService.FileChangedHandler(fileChangeSvc_FileChanged);
           this.serviceHost = new ServiceHost(fileChangeSvc);
           Trace.TraceInformation("Added FileChangeReceivingService as service host receiver.");
           // Recover the service in case of failure. 
           // Log the fault and attempt to restart the service host. 
           this.serviceHost.Faulted += (sender, e) =>
           {
               Trace.TraceError("Host fault occured. Aborting and restarting the host. Retry count: {0}", retries);
               this.serviceHost.Abort();
               this.StartService(--retries);
           };

           // use NetTcpBinding with no security
           NetTcpBinding binding = new NetTcpBinding(SecurityMode.None);
           Trace.TraceInformation("Initialized binding with name " + binding.Name);
        
           // define an internal endpoint for inter-role traffic
           RoleInstanceEndpoint internalEndPoint =
               RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["ddsynceventendpoint"];

           Trace.TraceInformation(string.Format("Endpoint {0}:{1} is configured for internal communication", internalEndPoint.IPEndpoint.Address.ToString(), internalEndPoint.IPEndpoint.Port));
           this.serviceHost.AddServiceEndpoint(
              typeof(IFileSync),
              binding,
              String.Format("net.tcp://{0}/ddsynceventendpoint", internalEndPoint.IPEndpoint));

           // create channel factory for inter-role communication
           factory = new ChannelFactory<IFileSync>(binding);

         
               Trace.TraceInformation("Starting DDFileSync service host successfully.");
               this.serviceHost.Open();
               Trace.TraceInformation("DDFileSync service host started successfully.");

               localTesting = bool.Parse(SyncService.GetConfigValueAsString("com.dynamicdeploy.ddfilesync.LocalTesting"));

               Trace.TraceInformation("Is this a test environment?" + localTesting);
               backupFileOnUpdateDelete = bool.Parse(SyncService.GetConfigValueAsString("com.dynamicdeploy.ddfilesync.BackupFileOnUpdateDelete"));

               Trace.TraceInformation("Backup files on Delete and Updates?" + backupFileOnUpdateDelete);
           }
           catch (TimeoutException timeoutException)
           {
               Trace.TraceError("The service operation timed out. {0}",
                                timeoutException.Message);

               this.StartService(--retries);
           }
           catch (CommunicationException communicationException)
           {
               Trace.TraceError("Could not start file sync service host. {0}",
                                communicationException.Message);
               this.StartService(--retries);
           }
           catch (Exception ex)
           {
               Trace.TraceError("General Exception while starting. {0}",
                              ex.Message);
               this.StartService(--retries);
           }
       }

        #endregion

        #region File Changed Remote Event
        void fileChangeSvc_FileChanged(object sender, FileChangeEventArgs fs)
       {
           
         
               try
               {
                   if (fs != null && fs.FileInfo != null && fs.FileInfo.AbsoluteFilePath != null && fs.FileInfo.LastModifiedTimestampUtc != DateTime.MaxValue && fs.FileInfo.LastModifiedTimestampUtc != DateTime.MinValue)
                   {
                       FileInformation fi = fs.FileInfo;
                       //Task.Factory.StartNew(() =>
                       //    {
                       //        ExecuteSyncProcessAfterRemoteEvent(fi, fs.FileChangeType);

                       //    }, TaskCreationOptions.PreferFairness);

                       Task t = new Task(() =>
                           {
                               ExecuteSyncProcessAfterRemoteEvent(fi, fs.FileChangeType);
                           }, TaskCreationOptions.PreferFairness);


                       t.Start();
                   }
                  
                   else
                   {
                       throw new ArgumentNullException("FileChangeEventArgs is null or LastModified timestamp was not provided. Ignoring the message.");

                   }

               }
               catch (Exception ex)
               {

                   Trace.TraceError("Error in fileChangeSvc_FileChanged on " + ex.Message);
               }

           
       }


        private void FileChangeByType(FileInformation fs, FileChangeType changeType)
        {
            //TODO: Exclusive file locking when events are stopped so that no other process changes the file
            FileSystemWatcher fsw = FolderWatchers[fs.AbsoluteFolderPath];


            try
            {
                switch (changeType)
                {

                    case FileChangeType.Create:
                        fsw.EnableRaisingEvents = false;
                        CreateOrUpdateFile(fs, changeType);
                       
                        break;
                    case FileChangeType.Delete:

                      

                        if (File.Exists(fs.AbsoluteFilePath))
                        {
                            BackupFileToStorage(fs.AbsoluteFilePath, fs.OriginatorRoleInstanceId, changeType);
                            int retries = 3;
                            while (retries > 0)
                            {
                                fsw.EnableRaisingEvents = false;
                                if (!FileUtils.IsFileInUse(fs.AbsoluteFilePath))
                                {
                                    try
                                    {
                                        File.Delete(fs.AbsoluteFilePath);
                                        retries = 0;

                                    }
                                    catch (Exception ey)
                                    {
                                        --retries;
                                        Trace.TraceError(string.Format("Could not secure exclusive delete access to file {0}. Exception:{1}", fs.AbsoluteFilePath, ey.Message));

                                    }
                                }
                                else
                                {
                                    --retries;
                                    Thread.Sleep(TimeSpan.FromSeconds(10));

                                }
                            }
                        }
                       
                        break;

                    case FileChangeType.Update:

                        fsw.EnableRaisingEvents = false;
                        CreateOrUpdateFile(fs, changeType);
                        
                        break;

                }

            }
            catch (Exception ex)
            {


                Trace.TraceError(string.Format("Exception while updating file {0}. Exception:{1}", fs.AbsoluteFilePath, ex.Message));

            }
            finally
            {

                fsw.EnableRaisingEvents = true;
            }


        }
       private void ExecuteSyncProcessAfterRemoteEvent(FileInformation fi, FileChangeType changeType)
       {

        
               switch (DdSyncType)
               {

                 
                   case SyncType.SyncOff:
                       //Do Nothing
                       break;
                   case SyncType.PeriodicSyncToStorageAndReplication:
                       //sync 
                       FileChangeByType(fi, changeType);
                       SyncDirectoryOnEvent(fi.AbsoluteFolderPath);
                   

                       break;
                  
                   case SyncType.PeriodicSyncToStorageOnly:
                       //Do nothing as SyncService executes independently
                       break;
                   case SyncType.SyncToStorageOnlyOnFileEvent:

                       //Run the sync only on event (assuming that sync service is not running). No replication
                         SyncDirectoryOnEvent(fi.AbsoluteFolderPath);

                       break;
                   case SyncType.ReplicationOnly:
                       FileChangeByType(fi, changeType);
                       break;
                   default:

                       Trace.TraceError("Unknown Sync Type " + DdSyncType.ToString());
                       break;


               };

         

       }
       private void CreateOrUpdateFile(FileInformation fs, FileChangeType changeType)
       {
           int retries = 3;
           bool success = false;
           while (retries > 0)
           {
               if (!FileUtils.IsFileInUse(fs.AbsoluteFilePath))
               {
                   try
                   {
                       if (!File.Exists(fs.AbsoluteFilePath))
                       {
                           File.WriteAllBytes(fs.AbsoluteFilePath, fs.FileContents);
                           File.SetLastWriteTimeUtc(fs.AbsoluteFilePath, fs.LastModifiedTimestampUtc);
                          
                       }
                       else
                       {
                          



                           if (File.GetLastWriteTimeUtc(fs.AbsoluteFilePath) < fs.LastModifiedTimestampUtc)
                           {
                               BackupFileToStorage(fs.AbsoluteFilePath, fs.OriginatorRoleInstanceId, changeType);
                               File.WriteAllBytes(fs.AbsoluteFilePath, fs.FileContents);
                               File.SetLastWriteTimeUtc(fs.AbsoluteFilePath, fs.LastModifiedTimestampUtc);
                             
                           }


                       }

                        retries = 0;
                           success = true;
                   }
                   catch (Exception ex)
                   {
                       --retries;
                       Trace.TraceError(string.Format("Error copying file {0}. Exception {1}", fs.AbsoluteFilePath, ex.Message));
                       Thread.Sleep(TimeSpan.FromSeconds(10));
                   }

               }
               else
               {
                   --retries;
                   Thread.Sleep(TimeSpan.FromSeconds(10));

               }

           }//while

           if (!success)
           {
               Trace.TraceError(string.Format("Error copying file {0}. Exceeded retries {1}", fs.AbsoluteFilePath, retries));

           }

       }
      
       private void BackupFileToStorage(string absoluteFilePath, string sourceRoleId, FileChangeType changeType)
       {
           if (backupFileOnUpdateDelete)
           {
               if (File.Exists(absoluteFilePath))
               {
                   try
                   {
                       string containerName = SyncService.GetConfigValueAsString("com.dynamicdeploy.ddfilesync.BackupContainerName");

                       CloudBlobContainer container = CloudStorageAccount.Parse(SyncService.GetConfigValueAsString("com.dynamicdeploy.ddfilesync.DataConnectionString")).CreateCloudBlobClient().GetContainerReference(containerName);

                       container.CreateIfNotExist();
                       DateTime dt = System.DateTime.UtcNow;

                       string blobName = string.Format("{0}/{1}/{2}/{3}/{4}-{5}{6}", dt.Year, dt.Month, dt.Day, RoleEnvironment.CurrentRoleInstance.Id, Path.GetFileNameWithoutExtension(absoluteFilePath), dt.Ticks.ToString(), Path.GetExtension(absoluteFilePath));

                       var newBlob = container.GetBlobReference(blobName);
                       newBlob.Metadata["absolutepath"] = HttpUtility.UrlEncode(absoluteFilePath);
                       newBlob.Metadata["sourceroleid"] = HttpUtility.UrlEncode(sourceRoleId);
                       newBlob.Metadata["destinationroleid"] = HttpUtility.UrlEncode(RoleEnvironment.CurrentRoleInstance.Id);
                       newBlob.Metadata["localchangetime"] = System.DateTime.UtcNow.Ticks.ToString();
                       newBlob.Metadata["changetype"] = changeType.ToString();

                       using (var stream = File.Open(absoluteFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
                       {
                           newBlob.UploadFromStream(stream, new BlobRequestOptions() { RetryPolicy = SyncService.RETRY_POLICY, Timeout = TimeSpan.FromSeconds(SyncService.BLOB_UPLOAD_TIMEOUT_SECS) });
                       }

                   }
                   catch (Exception ex)
                   {

                       Trace.TraceError(string.Format("Error backing up file {0}. Exception:{1}", absoluteFilePath, ex.Message));
                       throw ex;

                   }

               }

           }
       }
      
       #endregion

       #region Notify ALL
       /// <summary>
       /// Notifies all available worker roles to update their files
       /// </summary>
       /// <param name="session">The SessionInformation object for the client.</param>
       public void NotifyAllNodes(FileInformation session, FileChangeType changeType)
       {
           // iterate over all instances of the internal endpoint except the current role - no need to notify itself
           var current = RoleEnvironment.CurrentRoleInstance;
           IEnumerable<RoleInstanceEndpoint> endPoints = null;
           if (localTesting && RoleEnvironment.IsEmulated)
           {
               //In local testing, send events to yourself
               endPoints = current.Role.Instances
                          .Select(instance => instance.InstanceEndpoints["ddsynceventendpoint"]);
            

           }
           else
           {
               //In the cloud, don't send events to yourself
               endPoints = current.Role.Instances
                       .Where(instance => instance != current)
                       .Select(instance => instance.InstanceEndpoints["ddsynceventendpoint"]);

           }
         

           //foreach (var ep in endPoints)
           //{
           //    EndpointAddress address =
           //        new EndpointAddress(String.Format("net.tcp://{0}/ddsynceventendpoint", ep.IPEndpoint));
           //    IFileSync client = factory.CreateChannel(address);

           //    try
           //    {
           //        client.FileChangeMessage(session, changeType);
           //        ((ICommunicationObject)client).Close();
           //    }
           //    catch (TimeoutException timeoutException)
           //    {
           //        Trace.TraceError("Unable to notify worker role instance '{0}'. The service operation timed out. {1}", ep.RoleInstance.Id, timeoutException.Message);
           //        ((ICommunicationObject)client).Abort();
           //    }
           //    catch (CommunicationException communicationException)
           //    {
           //        Trace.TraceError("Unable to notify worker role instance '{0}'. There was a communication problem. {1} - {2}", ep.RoleInstance.Id, communicationException.Message, communicationException.StackTrace);
           //        ((ICommunicationObject)client).Abort();
           //    }
           //}

           Parallel.ForEach<RoleInstanceEndpoint>(endPoints, ep =>
           {
               EndpointAddress address =
                   new EndpointAddress(String.Format("net.tcp://{0}/ddsynceventendpoint", ep.IPEndpoint));
               int retries = 3;
               while (retries > 0)
               {

                   if(retries<3){
                       Thread.Sleep(TimeSpan.FromSeconds(60));
                   }
                   IFileSync client = factory.CreateChannel(address);

                   try
                   {
                       client.FileChangeMessage(session, changeType);
                       retries = 0;
                       ((ICommunicationObject)client).Close();
                   }
                   catch (TimeoutException timeoutException)
                   {
                       --retries;
                       Trace.TraceError("Unable to notify worker role instance '{0}'. The service operation timed out. {1}", ep.RoleInstance.Id, timeoutException.Message);
                       ((ICommunicationObject)client).Abort();
                   }
                   catch (CommunicationException communicationException)
                   {
                       --retries;
                       Trace.TraceError("Unable to notify worker role instance '{0}'. There was a communication problem. {1} - {2}", ep.RoleInstance.Id, communicationException.Message, communicationException.StackTrace);
                       ((ICommunicationObject)client).Abort();
                   }
               }


           });
       }


       #endregion
        #region File Watcher
       private FileSystemWatcher EnableFileSystemWatcher(FileSystemWatcher folderWatcher, string folderPath)
       {


           folderWatcher = new FileSystemWatcher(folderPath);
           folderWatcher.EnableRaisingEvents = true;
           folderWatcher.Filter = "*.*";
           folderWatcher.IncludeSubdirectories = true;
           folderWatcher.NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.LastWrite;
           folderWatcher.Created += new FileSystemEventHandler(FolderWatcher_Created);
           folderWatcher.Changed += new FileSystemEventHandler(FolderWatcher_Changed);
           folderWatcher.Deleted += new FileSystemEventHandler(FolderWatcher_Deleted);
           folderWatcher.Renamed += new RenamedEventHandler(FolderWatcher_Renamed);

           return folderWatcher;
       }

       #region Event Handlers

       private bool IsSyncedDirectory(string directoryPath)
       {
           if(SyncService.Instance != null)
           {

               return SyncService.Instance.GlobalAbsolutePathOfFoldersToInclude.Contains(directoryPath);
           }

           return false;


       }

       private void FireSyncProcess(FileInformation fi, FileChangeType changeType)
       {
           switch (DdSyncType)
           {

           
               case SyncType.SyncOff:
                   //Do Nothing
                   break;
               case SyncType.PeriodicSyncToStorageAndReplication:

                   SyncDirectoryOnEvent(fi.AbsoluteFolderPath);
                   if (File.Exists(fi.AbsoluteFilePath))
                   {
                       fi.FileContents = FileUtils.ReadFileWithoutLocking(fi.AbsoluteFilePath);
                       fi.DirectReplace = true;
                   }
                   fi.FileName = Path.GetFileName(fi.AbsoluteFilePath);
                   NotifyAllNodes(fi, changeType);

                   
                   break;

               case SyncType.PeriodicSyncToStorageOnly:
                   //Do nothing as SyncService executes independently
                   break;
               case SyncType.SyncToStorageOnlyOnFileEvent:

                   //Run the sync only on event (assuming that sync service is not running)
                   SyncDirectoryOnEvent(fi.AbsoluteFolderPath);
                   fi.DirectReplace = false;
                   fi.FileName = Path.GetFileName(fi.AbsoluteFilePath);
                   NotifyAllNodes(fi, changeType);
                   break;
               case SyncType.ReplicationOnly:

                   NotifyAllNodes(fi, changeType);
                   break;
               default:

                   Trace.TraceError("Unknown Sync Type " + DdSyncType.ToString());
                   break;


           };


       }

       private void SyncDirectoryOnEvent(string folderPath)
       {
           //Run sync only if it is a monitored directory
           if (IsSyncedDirectory(folderPath))
           {
               if (SyncService.Instance.SyncLockBlob == null)
                   SyncService.Instance.CreateSyncLockBlob();
               try
               {
                   using (var arl = new AutoRenewLease(SyncService.Instance.SyncLockBlob))
                   {
                       if (arl.HasLease)
                       {
                           FileSystemWatcher fs = FolderWatchers[folderPath];
                           fs.EnableRaisingEvents = false;
                           SyncService.Instance.SyncLocal(folderPath);
                           fs.EnableRaisingEvents = true;

                       }

                   }

               }
               catch (Exception)
               {
                   //ignore
               }

           }

       }

       private void HandleFolderWatcherEvent(FileSystemEventArgs e, FileChangeType changeType)
       {
           try
           {
               string filePath = e.FullPath;
               string dirPath = Path.GetDirectoryName(e.FullPath);

               if (SyncService.FileExtensionsToExclude != null && SyncService.FileExtensionsToExclude.Contains(Path.GetExtension(e.FullPath)))
               {
                   return;
               }
               if (SyncService.Instance.DirectoriesToExclude != null && (SyncService.Instance.DirectoriesToExclude.Contains(dirPath) || SyncService.Instance.DirectoriesToExclude.Contains(Path.GetDirectoryName(dirPath))))
               {
                   return;
               }
               //Notify only if the directory is a watched directory. All other directories will be ignored
               FileInformation fi = new FileInformation()
               {

                   AbsoluteFilePath = e.FullPath,
                   AbsoluteFolderPath = dirPath,
                   LastModifiedTimestampUtc = File.GetLastWriteTimeUtc(e.FullPath),
                   OriginatorRoleInstanceId = RoleEnvironment.CurrentRoleInstance.Id

               };

               FireSyncProcess(fi, changeType);

           }
           catch (Exception ex)
           {

               Trace.TraceError("Error in FolderWatcher_Created " + ex.Message);

           }

       }
       // A file was created. Create a task to upload the file as a blob.

       private void FolderWatcher_Created(object sender, FileSystemEventArgs e)
       {
           HandleFolderWatcherEvent(e, FileChangeType.Create);
         
       }


       // File was modified. Queue as task to upload the file as a blob.

       private void FolderWatcher_Changed(object sender, FileSystemEventArgs e)
       {
           HandleFolderWatcherEvent(e, FileChangeType.Update);
       }


       // A file was deleted. Queue a task to delete the corresponding blob.

       private void FolderWatcher_Deleted(object sender, FileSystemEventArgs e)
       {
           HandleFolderWatcherEvent(e, FileChangeType.Delete);
       }


       // A file was renamed. Queue a task to upload file as a blob.
       // TODO: delete previous file name from blob storage.

       void FolderWatcher_Renamed(object sender, RenamedEventArgs e)
       {
           HandleFolderWatcherEvent(e, FileChangeType.Create);
       }


       #endregion
        #endregion
    }
}
